Day 4 的商品快取每個 key 都設了 TTL,理由是「不設就永遠佔著記憶體」。
但設了 TTL 的 key,時間到的那一秒記憶體就還回來了嗎?並沒有。key 過期一分鐘之後,DBSIZE 還算得到它們,記憶體也還佔著。
今天沒有新指令,是拿幾個看得到狀態的指令來觀察:
-1 是沒設過期時間,-2 是不存在或已過期)兩種做法,兩種都不保證即時:
hz 次(預設 10),每次隨機抽 20 個設過 TTL 的 key 檢查,過期的超過 1/4 就再抽一輪。重點在「隨機抽 20 個」,是抽樣,不是掃全部。下面兩個範例就是同一套機制的兩個極端。
先灌 10 萬個不會過期的 key 當背景
(這兩行在主機的終端機下,不是在 redis-cli 裡面):
docker exec redis30days redis-cli -n 1 FLUSHDB
for i in $(seq 1 100000); do echo "SET bg:$i 1 EX 10000"; done | docker exec -i redis30days redis-cli -n 1 --pipe
-n 1 是切到 1 號資料庫。
再建 100 個各 1MB、一秒後就過期的 key(EVAL 只是拿來灌測試資料,Lua 是之後會遇到的事):
EVAL "local v=string.rep('x',1024*1024) for i=1,100 do redis.call('SET','big:'..i,v,'EX',1) end return 1" 0
DBSIZE # 100100
記憶體回主機的終端機看:
docker exec redis30days redis-cli INFO memory | grep used_memory_human

這 100 個 key 在第一秒就全部過期了。一分鐘之後再看一次:
DBSIZE

docker exec redis30days redis-cli INFO memory | grep used_memory_human

100100 少到 100085,100 個裡面只被清掉 15 個;記憶體從 139.49M 只掉到 118.21M,還回來 21MB。
算一下就知道為什麼:一秒抽十輪、一輪 20 個,等於一秒只檢查 200 個 key,而這裡一千個裡面才有一個是過期的。一秒抽中 0.2 個,一分鐘十幾個,跟量到的 15 個對得上。
換個極端,10 萬個 key 全部同時過期:
for i in $(seq 1 100000); do echo "SET all:$i 1 EX 1"; done | docker exec -i redis30days redis-cli -n 1 --pipe
DBSIZE # 100000,剛灌完
DBSIZE # 0,兩秒後再打一次
這次兩秒內清光。因為隨便抽 20 個都是過期的,遠超過 1/4 那個門檻,Redis 就一輪接一輪抽下去,抽到抽不太到為止。
同一套定期刪除,一邊一分鐘清掉 15 個,一邊兩秒清掉 10 萬個。差別只在「過期的 key 佔整體的比例」,跟過期多久完全無關。
回到上面那個「一分鐘只清掉 15 個」的狀態。KEYS 一個都查不到,DBSIZE 卻照樣把它們算進去:
KEYS big:* # (empty array)
DBSIZE # 100085
查詢類的指令會先判斷過沒過期,過期就當作不存在;DBSIZE 和 INFO keyspace 的 keys= 是直接報字典裡有幾筆。
接著讀一個已經過期的 key:
GET big:1 # nil
DBSIZE # 100084
GET 回 nil 是意料中的,但 DBSIZE 少了 1、記憶體也跟著掉,這就是惰性刪除。那個 key 不是自己消失的,是這次 GET 順手刪掉的。TTL、EXISTS 也會觸發,KEYS 不會。
補一句:惰性刪除和定期刪除都只在主節點上跑,從節點要等主節點的 DEL 複製過來才跟著刪,之後遇到主從節點再來看這件事。
DBSIZE 是「字典裡還有幾筆」,不是「現在有幾個 key 可以用」,過期沒刪的也算在內。那如果就是刪不夠快,記憶體先滿了會怎樣?
答案是「看設定」,而且預設值跟大部分人想的不一樣,Redis 預設根本不會自動淘汰任何東西,滿了就直接讓寫入報錯。明天繼續來看 maxmemory 跟八種淘汰策略![]()